/*
 * Copyright 2010-2011 CGT&DC ISA RAS
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package websolve.workflow.wfms;

import java.net.URI;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Logger;

import org.codehaus.jettison.json.JSONArray;
import org.codehaus.jettison.json.JSONObject;

import websolve.everest.Input;
import websolve.everest.Output;
import websolve.everest.ServiceDescription;
import websolve.workflow.runtime.AnyParameter;
import websolve.workflow.runtime.Parameter;
import websolve.workflow.runtime.Task;
import websolve.workflow.runtime.Workflow;
import websolve.workflow.runtime.script.ScriptTask;

public class JSONParser {

    public static Workflow parseWorkflow(JSONObject json, String user) throws Exception {
        log.fine("Started parsing workflow");
        log.finer("JSON: " + json);

        JSONObject props = json.getJSONObject("working").getJSONObject("properties");
        String name = props.getString("name");
        log.finer("Name: " + name);
        Workflow wf = new Workflow(name);

        // parse modules
        JSONArray modules = json.getJSONObject("working").getJSONArray("modules");
        Object[] moduleObjects = new Object[modules.length()];
        for (int i = 0; i < modules.length(); i++) {
            JSONObject module = modules.getJSONObject(i);
            JSONObject config = module.getJSONObject("config");
            String moduleType = config.getString("type");
            if (moduleType.equals("Task")) {

                String serviceUrl = config.getString("URL");
                ServiceTask task = new ServiceTask("task" + i + " (" + module.getString("name") + ")", new URI(
                        serviceUrl), user);
                wf.addTask(task);
                log.finer("Added service task: " + task.getName() + " (URI: " + serviceUrl + ")");

                moduleObjects[i] = task;

            } else if (moduleType.equals("input")) {

                String paramName = module.getString("name");
                ;
                String paramType = config.getJSONObject("schema").getString("type");
                wf.addInput(paramName, paramType);
                log.finer("Added input: " + paramName + " (type: " + paramType + ")");
                if (config.optBoolean("constant", false)) {
                    wf.setInputValue(paramName, config.getJSONObject("schema").getString("default"));
                    log.finer("Added input value: " + paramName + " = " + wf.getInputValue(paramName));
                }

                moduleObjects[i] = wf.getInput(paramName);

            } else if (moduleType.equals("output")) {

                String paramName = module.getString("name");
                ;
                String paramType = config.getJSONObject("schema").getString("type");
                wf.addOutput(paramName, paramType);
                log.finer("Added output: " + paramName + " (type: " + paramType + ")");

                moduleObjects[i] = wf.getOutput(paramName);

            } else if (moduleType.equals("Script")) {

                String func = config.getString("scriptText");
                String[] args = func.substring(func.indexOf("(") + 1, func.indexOf(")")).replaceAll("\\s", "")
                        .split(",");
                String funcBody = func.substring(func.indexOf("{") + 1, func.lastIndexOf("}"));
                String script = funcBody.replace("return", "out=");

                Task task = new ScriptTask("task" + i + " (" + module.getString("name") + ")", "JavaScript", script,
                        args, new String[] { "out" });

                wf.addTask(task);
                log.finer("Added JavaScript task: " + task.getName() + " (Script: " + script + ")");

                moduleObjects[i] = task;

            } else if (moduleType.equals("Python")) {

                String func = config.getString("scriptText");
                String[] args = func.substring(func.indexOf("(") + 1, func.indexOf(")")).replaceAll("\\s", "")
                        .split(",");
                String funcCall = func.substring(func.indexOf("def") + 3, func.indexOf(":")).replaceAll("\\s", "");
                String script = func + "\n" + "out=" + funcCall;

                Task task = new ScriptTask("task" + i + " (" + module.getString("name") + ")", "python", script, args,
                        new String[] { "out" });

                wf.addTask(task);
                log.finer("Added Python task: " + task.getName() + " (Script: " + script + ")");

                moduleObjects[i] = task;

            }
        }

        // parse wires
        JSONArray wires = json.getJSONObject("working").getJSONArray("wires");
        for (int i = 0; i < wires.length(); i++) {
            JSONObject wire = wires.getJSONObject(i);
            JSONObject src = wire.getJSONObject("src");
            int srcId = src.getInt("moduleId");
            Object srcModule = moduleObjects[srcId];
            String srcParam = src.getString("terminal").split(":")[0];
            JSONObject tgt = wire.getJSONObject("tgt");
            int tgtId = tgt.getInt("moduleId");
            Object tgtModule = moduleObjects[tgtId];
            String tgtParam = tgt.getString("terminal").split(":")[0];

            if (srcModule instanceof Parameter) { // input -> task
                Parameter input = ((Task) tgtModule).getInputs().get(tgtParam);
                if (!input.isConnected()) { // first input
                    ((Task) tgtModule).setInput(tgtParam, (Parameter) srcModule);
                } else if (!(input instanceof AnyParameter)) { // second input
                    AnyParameter anyInput = new AnyParameter(input.getType()).addParam(input).addParam(
                            (Parameter) srcModule);
                    ((Task) tgtModule).setInput(tgtParam, anyInput);
                } else { // third input and more...
                    ((AnyParameter) input).addParam((Parameter) srcModule);
                    ((Task) tgtModule).setInput(tgtParam, input);
                }
                log.finer("Connected [input]." + srcParam + " -> [" + ((Task) tgtModule).getName() + "]." + tgtParam);
            } else if (srcModule instanceof Task && tgtModule instanceof Task) { // task
                                                                                 // ->
                                                                                 // task
                Parameter input = ((Task) tgtModule).getInputs().get(tgtParam);
                if (!input.isConnected()) { // first input
                    ((Task) tgtModule).setInput(tgtParam, ((Task) srcModule).getOutput(srcParam));
                } else if (!(input instanceof AnyParameter)) { // second input
                    AnyParameter anyInput = new AnyParameter(input.getType()).addParam(input).addParam(
                            ((Task) srcModule).getOutput(srcParam));
                    ((Task) tgtModule).setInput(tgtParam, anyInput);
                } else { // third input and more...
                    ((AnyParameter) input).addParam(((Task) srcModule).getOutput(srcParam));
                    ((Task) tgtModule).setInput(tgtParam, input);
                }
                log.finer("Connected [" + ((Task) srcModule).getName() + "]." + srcParam + " -> ["
                        + ((Task) tgtModule).getName() + "]." + tgtParam);
            } else if (tgtModule instanceof Parameter) { // task -> output
                Parameter input = wf.getOutput(tgtParam);
                if (!input.isConnected()) { // first input
                    wf.setOutput(tgtParam, ((Task) srcModule).getOutput(srcParam));
                } else if (!(input instanceof AnyParameter)) { // second input
                    AnyParameter anyInput = new AnyParameter(input.getType()).addParam(input).addParam(
                            ((Task) srcModule).getOutput(srcParam));
                    wf.setOutput(tgtParam, anyInput);
                } else { // third input and more...
                    ((AnyParameter) input).addParam(((Task) srcModule).getOutput(srcParam));
                    wf.setOutput(tgtParam, input);
                }
                log.finer("Connected [" + ((Task) srcModule).getName() + "]." + srcParam + " -> [output]." + tgtParam);
            }
        }

        log.fine("Finished parsing workflow");

        return wf;
    }

    public static ServiceDescription parseService(String serviceId, JSONObject json, WorkflowManager wfMan)
            throws Exception {
        log.fine("Started parsing workflow " + serviceId);
        log.finer("JSON: " + json);

        JSONObject props = json.getJSONObject("working").getJSONObject("properties");
        String name = props.getString("name");
        log.fine("Name: " + name);
        String description = props.getString("description");
        log.fine("Description: " + description);

        // parse inputs/outputs

        List<Input> inParams = new ArrayList<Input>();
        Map<String, String> inFiles = new HashMap<String, String>();
        List<Output> outParams = new ArrayList<Output>();
        Map<String, String> outFiles = new HashMap<String, String>();

        JSONObject inputSchema = new JSONObject();
        inputSchema.put("id", "input");
        inputSchema.put("type", "object");
        JSONObject inputSchemaProps = new JSONObject();
        inputSchema.put("properties", inputSchemaProps);

        JSONArray modules = json.getJSONObject("working").getJSONArray("modules");
        for (int i = 0; i < modules.length(); i++) {
            JSONObject module = modules.getJSONObject(i);
            JSONObject config = module.getJSONObject("config");
            String moduleType = config.getString("type");
            if (moduleType.equals("input")) {

                boolean isConstant = config.optBoolean("constant", false);
                if (!isConstant) {
                    String paramName = module.getString("name");
                    JSONObject paramSchema = config.getJSONObject("schema");

                    Input param = new Input(paramName, paramSchema.getString("type"), !paramSchema.optBoolean(
                            "optional", false), paramSchema.optString("default", null), paramSchema.optString("title",
                            null), paramSchema.optString("description", null));

                    inParams.add(param);
                    inputSchemaProps.put(paramName, paramSchema);
                    log.fine("Added input param: " + paramName + " (type: " + param.getType() + ")");
                    if (param.getType().equals("file")) {
                        inFiles.put(paramName, paramName);
                        log.fine("Added input file: " + paramName);
                    }
                } else {
                    log.fine("Ignored constant param: "
                            + module.getJSONObject("value").getJSONObject("input").getJSONObject("inputParams")
                                    .getString("name"));
                }

            } else if (moduleType.equals("output")) {

                String paramName = module.getString("name");
                JSONObject paramSchema = config.getJSONObject("schema");

                Output param = new Output(paramName, paramSchema.getString("type"),
                        paramSchema.optString("title", null), paramSchema.optString("description", null));

                outParams.add(param);
                log.fine("Added output param: " + paramName + " (type: " + param.getType() + ")");
                if (param.getType().equals("file")) {
                    outFiles.put(paramName, paramName);
                    log.fine("Added output file: " + paramName);
                }

            }
        }

        ServiceDescription desc = new ServiceDescription(name, description, inParams, outParams, inputSchema);

        log.fine("Finished parsing workflow");
        return desc;
    }

    private static final Logger log = Logger.getLogger(JSONParser.class.getName());

}
